다층 퍼셉트론(MLP)의 역사
Minsky와 Papert는 단층의 한계를 지적했지만, 동시에 다층 퍼셉트론(Multi-Layer Perceptron, MLP)은 XOR 문제를 해결할 수 있다는 가능성을 언급했다. 은닉층(hidden layer)을 도입하면 선형적으로 분리 불가능한 문제도 해결할 수 있는 비선형 결정 경계를 만들 수 있다는 것이었다.
1980년대 중반, 여러 연구자들(특히 David Rumelhart, Geoffrey Hinton, Ronald Williams 등)이 다층 신경망을 효율적으로 훈련시킬 수 있는 역전파(Backpropagation) 알고리즘을 재발견하고 발전시켰다. 이 알고리즘은 다층 퍼셉트론의 가중치를 효과적으로 조정할 수 있게 하여, XOR 문제와 같은 비선형 문제를 성공적으로 해결할 수 있게 했다.
XOR 문제란?
XOR(Exclusive OR)는 두 입력이 서로 다를 때만 1을 출력하는 논리 연산이다. XOR 진리표는 다음과 같다:
| Input 1 | Input 2 | Output |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
이 문제의 특징은 입력 공간에서 출력이 1인 점들 (0,1), (1,0)과 출력이 0인 점들 (0,0), (1,1)을 단일 직선으로 분리할 수 없다는 것이다. 즉, 선형적으로 분리 불가능한(non-linearly separable) 문제이다.
다층 퍼셉트론은 은닉층을 추가함으로써 비선형 변환을 수행할 수 있다. 은닉층의 뉴런들이 입력 공간을 변환하여, 원래 선형적으로 분리 불가능했던 문제를 선형적으로 분리 가능한 공간으로 매핑한다.
XOR 문제의 경우, 최소한 하나의 은닉층이 있는 2층 신경망(입력층-은닉층-출력층)으로 해결할 수 있다. 은닉층의 뉴런들이 입력을 비선형적으로 변환하여, 출력층에서는 선형 분리가 가능한 형태로 만들어준다.
이제 실제로 XOR 문제를 해결하는 다층 퍼셉트론을 구현해보자.
import pandas as pd
import numpy as np
# 입력 데이터 (XOR 문제)
# 각 행은 [입력1, 입력2, bias(항상 1)]를 의미
# 마지막 열의 1은 bias term (편향 항)
X = np.array([
[0, 0, 1],
[0, 1, 1],
[1, 0, 1],
[1, 1, 1]
])
y = np.array([[0], [1], [1], [0]])
np.random.seed(42)
W1 = 2 * np.random.random((4, 3)) - 1 # hidden layer
W2 = 2 * np.random.random((1, 4)) - 1 # output layerMLP에서 활성화 함수(Activation Function)가 중요한 이유
활성화 함수는 신경망의 각 뉴런에서 입력 신호의 가중합을 받아 최종 출력을 결정하는 비선형 함수이다. 신경망의 각 층에서 가중치와 입력의 선형 결합(weighted sum)을 계산한 후, 활성화 함수를 적용하여 비선형 변환을 수행한다.
만약 활성화 함수가 없다면, 다층 신경망도 결국 선형 변환의 합성일 뿐이다. 예를 들어, 두 개의 선형 층이 있다면:
이는 단일 선형 층과 동일하다. 따라서 활성화 함수 없이는 다층 신경망이 단일층과 동일한 표현력을 가지게 되어, XOR 문제와 같은 비선형 문제를 해결할 수 없다.
비선형 활성화 함수를 사용하면 신경망은 복잡한 비선형 함수를 근사할 수 있다. Universal Approximation Theorem에 따르면, 충분한 수의 뉴런과 적절한 활성화 함수를 가진 단일 은닉층 신경망은 임의의 연속 함수를 임의의 정확도로 근사할 수 있다.
활성화 함수는 미분이 가능해야 역전파 알고리즘을 통해 가중치를 업데이트할 수 있다. 이전에 단층 퍼셉트론 AND 게이트 학습에 사용했던 계단함수의 경우 미분이 불가능한 지점이 있으므로 다층 퍼셉트론에서는 사용할 수 없다.
활성화 함수 Sigmoid
시그모이드 함수는 가장 전통적인 활성화 함수 중 하나로, 다음과 같이 정의된다:
시그모이드 함수의 특징
-
출력 범위: 0과 1 사이의 값을 출력한다. 이는 확률로 해석하기 좋아 이진 분류 문제의 출력층에서 자주 사용된다.
-
미분 가능: 모든 구간에서 미분 가능하며, 도함수가 간단한 형태로 표현된다:
-
부드러운 곡선: 입력값이 작을 때와 클 때 모두 부드럽게 변화하여, 학습 과정에서 안정적인 기울기를 제공한다.
시그모이드 함수는 기울기 소멸 문제(Vanishing Gradient Problem)로 인해, 깊은 신경망에서는 은닉층 활성화 함수로 잘 사용되지 않으며 ReLU 계열이 주로 사용된다. 다만 은닉층이 1개인 얕은 다층 퍼셉트론의 경우, 기울기가 연쇄적으로 곱해지는 횟수가 적기 때문에 기울기 소멸 문제가 비교적 덜 심각하며, 학습 목적이나 실습에서는 시그모이드를 사용해도 정상적으로 동작한다. 기울기 소멸에 관한 내용은 밑에서 더 자세히 다룬다.
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
return x * (1 - x)오차 역전파 (Error Backpropagation)
역전파(Backpropagation)는 다층 퍼셉트론을 학습시키기 위한 핵심 알고리즘이다. 오차 역전파 알고리즘은 출력층에서 발생한 오차를 역방향으로 전파하여 각 층의 가중치를 업데이트한다.
최적의 가중치를 찾는다는 말은 미분을 통해 가중치의 변화에 따라 오차가 얼마나 변하는지()를 파악해 이를 최소화할 수 있는 가중치를 만든다는 것이다.
그러나 다층 신경망에서는 오차 를 가중치 로 직접 미분할 수 없다. 왜냐하면 는 여러 층을 거쳐 계산되는 합성 함수이기 때문이다. 예를 들어, 이고 이므로, 는 과 를 거쳐 간접적으로 의존한다. 따라서 연쇄 법칙을 사용하여 각 층을 거쳐 역방향으로 기울기를 전파해야 한다.
연쇄 법칙 (Chain Rule)이란?
합성함수의 미분은 각 함수를 구성하는 함수의 미분의 곱으로 나타낼 수 있다. 신경망에서 손실 함수 이 여러 층을 거쳐 계산되므로, 각 가중치에 대한 기울기를 구하기 위해서는 연쇄 법칙이 필요하다.
역전파에서 연쇄 법칙의 핵심 아이디어는 다음과 같다:
신호 E(오차)에 노드의 국소적 미분(로컬 그래디언트)을 곱한 후 → 다음 노드에 전달하는 것
연쇄 법칙의 동작 과정:
- 출력층: 오차 신호 에 출력층의 국소적 미분 를 곱하여 을 계산
- 은닉층: 에 가중치 를 곱한 후, 은닉층의 국소적 미분 을 곱하여 을 계산
- 입력층: 을 사용하여 입력층의 가중치 에 대한 기울기를 계산
이렇게 각 노드에서 국소적 미분을 계산하고, 이전 층에서 전달된 오차 신호와 곱하여 다음 층으로 전파하는 것이 연쇄 법칙의 핵심이다.
역전파 알고리즘의 동작 원리
역전파 알고리즘은 다음과 같은 단계로 구성된다:
- 순전파(Forward Propagation): 입력 데이터를 신경망을 통해 전달하여 예측값을 계산한다.
- 손실 계산(Loss Calculation): 예측값과 실제값의 차이를 계산한다.
- 역전파(Backward Propagation): 출력층에서 시작하여 각 층의 오차 신호(델타)를 계산한다.
- 가중치 업데이트(Weight Update): 계산된 오차 신호를 사용하여 경사 하강법으로 가중치를 업데이트한다.
역전파 알고리즘은 연쇄 법칙 (Chain Rule)을 사용하여 각 가중치에 대한 손실 함수의 기울기를 계산한다.
출력층의 오차 신호
출력층의 오차 신호는 다음과 같이 계산된다:
여기서:
- : 예측 오차
- : 시그모이드 함수의 도함수
은닉층의 오차 신호
은닉층의 오차 신호는 출력층의 오차 신호를 역방향으로 전파하여 계산한다:
여기서:
- : 출력층 오차를 은닉층으로 전파
- : 은닉층 활성화 함수의 도함수
가중치 업데이트
각 가중치 행렬은 다음과 같이 업데이트된다:
여기서 는 학습률(learning rate)이다.
알고리즘의 장점
- 효율성: 모든 가중치에 대한 기울기를 한 번의 순전파와 역전파로 계산할 수 있다.
- 일반화: 여러 층으로 확장 가능하며, 다양한 활성화 함수와 손실 함수에 적용할 수 있다.
- 자동 미분: 연쇄 법칙을 통해 복잡한 미분 계산을 자동으로 수행한다.
구현 코드 설명
아래 코드는 3×4×1 구조의 다층 퍼셉트론을 역전파 알고리즘으로 학습하는 과정을 보여준다:
- 순전파: 입력 X를 은닉층을 거쳐 출력층까지 전달
- 손실 계산: 평균 제곱 오차(MSE)를 사용
- 역전파: 출력층에서 은닉층으로 오차 신호 전파
- 가중치 업데이트: 경사 하강법으로 가중치 조정
alpha = 0.9
epochs = 10000
# 가중치와 오차의 변화를 기록
hist_w = []
hist_loss = []
for epoch in range(epochs):
# ===== 순전파 =====
z1 = np.dot(X, W1.T) # (4, 4)
a1 = sigmoid(z1) # hidden activation
z2 = np.dot(a1, W2.T) # (4, 1)
y_hat = sigmoid(z2) # output
# ===== 손실 계산 =====
error = y - y_hat
loss = np.mean(error ** 2)
hist_loss.append(loss)
# ===== 역전파 =====
d_out = error * sigmoid_derivative(y_hat) # (4, 1)
d_hidden = np.dot(d_out, W2) * sigmoid_derivative(a1) # (4, 4)
# ===== 가중치 업데이트 =====
W2 += alpha * np.dot(d_out.T, a1)
W1 += alpha * np.dot(d_hidden.T, X)
hist_w.append({
"W1": W1.copy(),
"W2": W2.copy()
})
if epoch % 2000 == 0:
print(f"Epoch {epoch}, Loss: {loss:.6f}")
# ===== 최종 결과 =====
print("\nFinal prediction:")
print(np.round(y_hat, 3))Epoch 0, Loss: 0.263420 Epoch 2000, Loss: 0.000977 Epoch 4000, Loss: 0.000357 Epoch 6000, Loss: 0.000207 Epoch 8000, Loss: 0.000142 Final prediction: [[0.005] [0.989] [0.989] [0.013]]
import matplotlib.pyplot as plt
# 학습 과정에서 오차(delta) 변화 시각화
# epoch가 증가할수록 오차가 감소하는지 확인
plt.plot(hist_loss)
plt.xlabel("epoch")
plt.ylabel("delta (error)")
plt.title("Error Change During Training")
plt.show()# 은닉층 0번 뉴런의 가중치 추적
w_input1 = [w["W1"][0, 0] for w in hist_w]
w_input2 = [w["W1"][0, 1] for w in hist_w]
w_bias = [w["W1"][0, 2] for w in hist_w]
plt.plot(w_input1, label="input1")
plt.plot(w_input2, label="input2")
plt.plot(w_bias, label="bias")
plt.xlabel("Epoch")
plt.ylabel("Weight value")
plt.title("Hidden Neuron 0 Weight Changes")
plt.legend()
plt.grid(True)
plt.show()# ===== grid 생성 =====
xx, yy = np.meshgrid(
np.linspace(-0.1, 1.5, 300),
np.linspace(-0.1, 1.5, 300)
)
grid = np.c_[xx.ravel(), yy.ravel(), yy.ravel() * 0 + 1] # bias = 1
# ===== 모델 예측 =====
z1 = np.dot(grid, W1.T)
a1 = sigmoid(z1)
z2 = np.dot(a1, W2.T)
y_pred = sigmoid(z2)
Z = (y_pred > 0.5).reshape(xx.shape)
# ===== figure =====
plt.figure(figsize=(6, 6))
# 데이터 포인트
plt.scatter(X[y.flatten() == 0, 0], X[y.flatten() == 0, 1],
c='red', s=150, marker='o', label='Output = 0',
edgecolors='black', linewidths=2, alpha=0.7)
plt.scatter(X[y.flatten() == 1, 0], X[y.flatten() == 1, 1],
c='blue', s=150, marker='s', label='Output = 1',
edgecolors='black', linewidths=2, alpha=0.7)
# ===== decision boundary (곡선) =====
plt.contour(xx, yy, Z, levels=[0.5],
colors='green', linewidths=3, alpha=0.8)
# 축선 (AND 코드와 동일)
plt.axhline(y=0, color='black', linewidth=1, alpha=0.5)
plt.axvline(x=0, color='black', linewidth=1, alpha=0.5)
plt.xlim(-0.1, 1.5)
plt.ylim(-0.1, 1.5)
plt.xlabel('x1')
plt.ylabel('x2')
plt.title('XOR Decision Boundary')
plt.grid(True, alpha=0.3, linestyle='--')
plt.legend(fontsize=12, loc='upper right')
plt.show()핵심 개념 정리
-
다층 퍼셉트론(MLP): 은닉층을 가진 신경망으로, 단층 퍼셉트론이 해결할 수 없는 XOR 같은 비선형 문제를 해결할 수 있다.
-
XOR 문제: 단일 직선으로 분리할 수 없는 선형 분리 불가능한 문제로, 다층 퍼셉트론의 필요성을 보여주는 대표적인 예시이다.
-
활성화 함수: 신경망에 비선형성을 도입하여 복잡한 함수를 근사할 수 있게 하며, 역전파를 위해 미분 가능해야 한다.
-
시그모이드 함수: 0과 1 사이의 값을 출력하는 전통적인 활성화 함수로, 도함수가 로 간단하게 표현된다.
-
역전파(Backpropagation): 출력층에서 발생한 오차를 역방향으로 전파하여 각 층의 가중치에 대한 기울기()를 계산하는 알고리즘이다.
-
연쇄 법칙(Chain Rule): 합성 함수의 미분을 각 함수의 미분의 곱으로 계산하는 방법으로, 역전파에서 오차 신호에 국소적 미분을 곱해 다음 층으로 전파하는 핵심 원리이다.
-
순전파(Forward Propagation): 입력 데이터를 신경망을 통해 전달하여 예측값을 계산하는 과정이다.
-
기울기 소멸 문제: 깊은 신경망에서 시그모이드 함수의 도함수가 1보다 작아 역전파 과정에서 기울기가 0에 가까워지는 현상으로, ReLU 같은 활성화 함수로 완화할 수 있다.